package city.spring.modules.work.action;

import city.spring.modules.work.action.handler.IHandler;
import city.spring.modules.work.action.handler.TodayHasPeopleHandler;
import city.spring.modules.work.action.handler.TodayShiftHasPeopleHandler;
import city.spring.modules.work.action.handler.YesterdayHasPeopleHandler;
import city.spring.modules.work.action.model.AbstractPlanShift;
import city.spring.modules.work.action.model.AbstractPlanUser;
import city.spring.modules.work.action.model.ResultItem;
import com.google.common.collect.HashBasedTable;
import com.google.common.collect.Table;
import lombok.Data;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import reactor.function.Consumer3;

import java.time.LocalDate;
import java.util.*;
import java.util.stream.Collectors;

/**
 * 排班计算工具
 *
 * @author HouKunLin
 * @date 2020/6/4 0004 16:27
 */
@Data
public class WorkPlanCalculationUtils<SHIFT, USER> implements Runnable {
    private static final Logger logger = LoggerFactory.getLogger(WorkPlanCalculationUtils.class);
    private final boolean isDebug = logger.isDebugEnabled();
    /**
     * 是否打开日志
     */
    private boolean openLogger = false;
    /**
     * 该方案关联的上班时间段（班次）
     */
    private List<AbstractPlanShift<SHIFT>> shifts;
    /**
     * 该方案关联的人员列表信息
     */
    private List<AbstractPlanUser<USER>> users;
    /**
     * 开始执行日期
     */
    private LocalDate startDate;
    /**
     * 结束执行日期
     */
    private LocalDate endDate;
    /**
     * 工作日
     */
    private Set<String> workingDaySet;
    /**
     * 排班结果表（表格类型）
     */
    private Table<LocalDate, Object, ResultItem<SHIFT, USER>> resultTable = HashBasedTable.create();
    /**
     * 所有结果列表（一维列表）
     */
    private List<ResultItem<SHIFT, USER>> allResultItems = new ArrayList<>();
    /**
     * 条件处理
     */
    private WorkPlanRuleHandler<SHIFT, USER> handler = new WorkPlanRuleHandler<SHIFT, USER>(resultTable, new IHandler[]{
            // 同班次不重复
            new TodayShiftHasPeopleHandler<SHIFT, USER>(),
            // 同日不重复
            new TodayHasPeopleHandler<SHIFT, USER>(),
            // 夜班第二日不上班
            new YesterdayHasPeopleHandler<SHIFT, USER>()
    });

    /**
     * 失败单元格数（被重试的单元格数）
     */
    private int cellErrorNumber = 0;
    /**
     * 总执行获取人员次数
     */
    private int getPeopleNumber = 0;
    /**
     * 单次最大重试次数
     */
    private int maxRetryNumber = 0;
    /**
     * 是否尽最大程度完成排班运算（启用该操作会比普通操作消耗更多的资源，但是排班结果会相对均匀）
     */
    private boolean isCompulsoryCompletion = true;

    /**
     * 创建一个排班执行对象
     *
     * @param startDate     开始日期
     * @param endDate       结束日期
     * @param shifts        上班班次
     * @param users         参与用户
     * @param workingDaySet 工作日
     */
    public WorkPlanCalculationUtils(LocalDate startDate, LocalDate endDate, List<AbstractPlanShift<SHIFT>> shifts, List<AbstractPlanUser<USER>> users, Set<String> workingDaySet) {
        this.startDate = startDate;
        this.endDate = endDate;
        this.shifts = shifts;
        this.users = users;
        this.workingDaySet = workingDaySet;
    }

    /**
     * 自动配置参数
     */
    public void autoConfig() {
        // 初步计算每日每个班次的上班人数
        configNotLockWorkNumber();
        fillAllCell();
        // printResultTable();
        // 二次计算（调整）：被锁定上班次数的人员
        configLockUserWorkNumber();
        // printResultTable();
        configTodayNotWorkNumber();
        // 三次计算（调整）：夜班人员
        // printResultTable();
    }

    /**
     * 计算未锁定的上班次数信息
     */
    private void configNotLockWorkNumber() {
        // 获取未限定班次人数的班次列表
        List<AbstractPlanShift<SHIFT>> notLockWorkNumberShifts = shifts.stream()
                .filter(item -> item.getWorkNumber() == -1)
                .collect(Collectors.toList());
        if (notLockWorkNumberShifts.isEmpty()) {
            return;
        }
        // 获取未限定上班次数的人员列表
        List<AbstractPlanUser<USER>> notLockWorkNumberUsers = users.stream()
                .filter(item -> item.getWorkNumber() == -1)
                .collect(Collectors.toList());

        // 限制一个班次有多少人在上班： 某班次固定的上班人数
        int shiftLockWorkNumberSum = shifts.stream()
                .filter(item -> item.getWorkNumber() != -1)
                .map(AbstractPlanShift::getWorkNumber)
                .reduce(Integer::sum)
                .orElse(0);

        int canUseUserNumber = notLockWorkNumberUsers.size() - shiftLockWorkNumberSum;
        // 计算每个班次的上班人数
        int shiftNumber = canUseUserNumber / notLockWorkNumberShifts.size();
        // 取余，剩余到少人
        int shiftNumberMod = canUseUserNumber % notLockWorkNumberShifts.size();
        if (openLogger && isDebug) {
            logger.debug("每班次上班人数初始化配置 - 本次排班共涉及{}人，每日{}个班次", users.size(), shifts.size());
            logger.debug("每班次上班人数初始化配置 - 可使用人员数 {} 人 全部未被锁定班次数量 {} 个 ",
                    canUseUserNumber, notLockWorkNumberShifts.size());
            logger.debug("每班次上班人数初始化配置 - 平均每个班次增加 {} 人，剩余需要分配 {} 人", shiftNumber, shiftNumberMod);
        }

        // 自动计算每个班次所需要的上班人数
        for (AbstractPlanShift<SHIFT> shift : notLockWorkNumberShifts) {
            if (shiftNumberMod <= 0) {
                shift.setAutoWorkNumber(shiftNumber);
            } else {
                shift.setAutoWorkNumber(shiftNumber + 1);
                shiftNumberMod -= 1;
            }
        }
    }

    /**
     * 调整配置人员被锁定的上班次数
     */
    private void configLockUserWorkNumber() {
        // 被限制上班次数的人员列表信息
        List<AbstractPlanUser<USER>> lockWorkNumberUsers = users.stream()
                .filter(item -> item.getWorkNumber() != -1).collect(Collectors.toList());
        // 所有被限制的上班次数总和
        int userAllLockWorkNumber = users.stream()
                .filter(item -> item.getWorkNumber() != -1)
                .map(AbstractPlanUser::getWorkNumber)
                .reduce(Integer::sum)
                .orElse(0);
        // 把被限制的上班次数加入到班次人员数中

        // 所有未被限制限制班次人员数的班次列表
        List<ResultItem<SHIFT, USER>> notLockWorkNumberShifts = allResultItems.stream()
                .filter(item -> !item.isLockSize())
                .collect(Collectors.toList());

        if (notLockWorkNumberShifts.isEmpty()) {
            return;
        }

        // 未锁定的班次列表
        List<AbstractPlanShift<SHIFT>> notLockShifts = shifts.stream()
                .filter(item -> item.getWorkNumber() == -1)
                .collect(Collectors.toList());
        int workNumber = userAllLockWorkNumber / notLockWorkNumberShifts.size();
        int workNumberMod = userAllLockWorkNumber % notLockWorkNumberShifts.size();
        if (openLogger && isDebug) {
            logger.debug("每班次上班人数调整 - 被限制上班次数的人调整到班次中 - 所有被限制的上班次数总和 {} 全部未被锁定班次数量 {} 单日未被锁定班次数量 {}",
                    userAllLockWorkNumber, notLockWorkNumberShifts.size(), notLockShifts.size());
            logger.debug("每班次上班人数调整 - 被限制上班次数的人调整到班次中 - 平均每日增加 {} ，剩余需要分配 {}", workNumber, workNumberMod);
        }

        if (workNumber > 0) {
            // 重新调整班次的上班人数
            for (ResultItem<SHIFT, USER> item : notLockWorkNumberShifts) {
                int size = item.getSize();
                item.setSize(size + workNumber);
            }
        }

        if (workNumberMod <= 0) {
            return;
        }

        // 调整剩余的上班人次数；
        int notLockShiftSize = notLockShifts.size();
        for (int i = 0; i < notLockShiftSize; i++) {
            for (int cellIndex = 0; cellIndex < notLockWorkNumberShifts.size(); cellIndex++) {
                ResultItem<SHIFT, USER> item = notLockWorkNumberShifts.get(cellIndex);
                int size = item.getSize();
                if (cellIndex % notLockShiftSize == i) {
                    item.setSize(size + 1);
                    workNumberMod -= 1;
                }
                if (workNumberMod <= 0) {
                    break;
                }
            }
            if (workNumberMod <= 0) {
                break;
            }
        }
    }

    /**
     * 配置昨夜晚班的上班人员第二日休假
     */
    public void configTodayNotWorkNumber() {
        Map<LocalDate, Map<Object, ResultItem<SHIFT, USER>>> rowMap = resultTable.rowMap();
        for (Map.Entry<LocalDate, Map<Object, ResultItem<SHIFT, USER>>> rowMapEntry : rowMap.entrySet()) {
            // 每一天的班次信息
            LocalDate rowKey = rowMapEntry.getKey();
            Map<Object, ResultItem<SHIFT, USER>> colMap = rowMapEntry.getValue();
            // 这一日的所有班次列表
            Collection<ResultItem<SHIFT, USER>> items = colMap.values();
            // 被固定上班人数的班次列表
            List<ResultItem<SHIFT, USER>> lockSizeItems = items.stream()
                    .filter(ResultItem::isLockSize)
                    .collect(Collectors.toList());
            // 未被固定上班人数的班次列表
            List<ResultItem<SHIFT, USER>> notLockSizeItems = items.stream()
                    .filter(item -> !item.isLockSize())
                    .collect(Collectors.toList());
            // 次日不用上班的班次列表
            List<ResultItem<SHIFT, USER>> tomorrowNotWorkItems = items.stream()
                    .filter(item -> item.getShift().isTomorrowNotWork())
                    .collect(Collectors.toList());

            // 这一日的总上班人次数
            int allSizeSum = items.stream().map(ResultItem::getSize).reduce(Integer::sum).orElse(0);
            // 被固定班次上班人数总数
            int lockSizeSum = lockSizeItems.stream().map(ResultItem::getSize).reduce(Integer::sum).orElse(0);

            // 次日不上班，并且是被限制上班人数的总和
            int tomorrowNotWorkLockSizeSum = tomorrowNotWorkItems.stream()
                    .filter(ResultItem::isLockSize)
                    .map(ResultItem::getSize)
                    .reduce(Integer::sum)
                    .orElse(0);

            // 能够用来计算的班次总数
            int canUseSize = allSizeSum - lockSizeSum - tomorrowNotWorkLockSizeSum;

            // 次日不上班，并且可以调整上班人数的班次列表
            List<ResultItem<SHIFT, USER>> tomorrowNotWorkNotLockItems = tomorrowNotWorkItems.stream()
                    .filter(item -> !item.isLockSize())
                    .collect(Collectors.toList());

            // 需要更改上班人次的班次数
            int changeItemLen = notLockSizeItems.size() + tomorrowNotWorkNotLockItems.size();

            if (changeItemLen == 0) {
                return;
            }

            int baseNumber = canUseSize / changeItemLen;
            int baseNumberMod = canUseSize % changeItemLen;

            if (openLogger && isDebug) {
                logger.debug("每班次上班人数调整 - 夜班次日不休息的班次人数调整 - {}", rowKey);
                logger.debug("                                                         - 本日 {} 人上班，{} 人无法参与班次人数调整，{} 人参与班次人数调整",
                        allSizeSum, lockSizeSum, canUseSize);
                logger.debug("                                                         - 本日 {} 个班次，{} 个班次可调整人数，{} 个班次次日不上班，使用 {} 个班次来计算(可调整+次数不上班)",
                        items.size(), notLockSizeItems.size(), tomorrowNotWorkItems.size(), changeItemLen);
                logger.debug("                                                         - 平均 {} 人/班（共{}班），剩余 {} 人需要分配，调整后本日 {} 人上班",
                        baseNumber, notLockSizeItems.size(), baseNumberMod, baseNumber * notLockSizeItems.size() + baseNumberMod);
            }

            for (ResultItem<SHIFT, USER> notLockSizeItem : notLockSizeItems) {
                if (baseNumberMod <= 0) {
                    notLockSizeItem.setSize(baseNumber);
                } else {
                    notLockSizeItem.setSize(baseNumber + 1);
                    --baseNumberMod;
                }
            }
        }
    }

    /**
     * 填充所有单元格的框架数据
     */
    private void fillAllCell() {
        // 循环每一天 (循环排班天数)
        for (LocalDate toDay = startDate; toDay.compareTo(endDate) <= 0; toDay = toDay.plusDays(1)) {
            // 循环排班班次
            for (AbstractPlanShift<SHIFT> shift : shifts) {
                Object colKey = shift.getKey();
                // 向该天的该上班班次加入人员列表List
                if (workingDaySet.contains(String.valueOf(toDay.getDayOfWeek().getValue()))) {
                    // 工作日，填充基础配置框架
                    ResultItem<SHIFT, USER> item = new ResultItem<>(toDay, shift);
                    resultTable.put(toDay, colKey, item);
                    allResultItems.add(item);
                }
            }
        }
    }

    /**
     * 计算排班
     */
    @Override
    public void run() {
        try {
            for (Map.Entry<LocalDate, Map<Object, ResultItem<SHIFT, USER>>> rowMap : resultTable.rowMap().entrySet()) {
                LocalDate rowMapKey = rowMap.getKey();
                ArrayList<ResultItem<SHIFT, USER>> items = new ArrayList<>(rowMap.getValue().values());
                if (isCompulsoryCompletion) {
                    // 排序：上班人数少的班次排在前面，尽量满足人数少的班次
                    items.sort(Comparator.comparingInt(ResultItem::getSize));
                }
                for (ResultItem<SHIFT, USER> item : items) {
                    try {
                        // 填充人员信息
                        fillUser(item);
                    } catch (Exception ignore) {
                        // 捕获异常，并且不处理异常，是为了尽最大可能的完成排班
                    }
                }
            }
            printResultTable(true);
        } catch (Exception exception) {
            printResultTable(true);
            exception.printStackTrace();
        }
    }

    public void printResultTable() {
        printResultTable(false);
    }

    public void printResultTable(boolean showUser) {
        if (showUser) {
            for (AbstractPlanUser<USER> user : users) {
                System.out.println(user + " user.canUse = " + user.canUse());
            }
        }
        for (Map.Entry<LocalDate, Map<Object, ResultItem<SHIFT, USER>>> entry : resultTable.rowMap().entrySet()) {
            LocalDate date = entry.getKey();
            System.out.println(String.format("日期 %s 周 %s", date, date.getDayOfWeek().getValue()));
            entry.getValue().forEach((s, item) -> {
                List<AbstractPlanUser<USER>> list = item.getUsers();
                List<String> collect = list.stream()
                        .map(AbstractPlanUser::getTitle)
                        .collect(Collectors.toList());
                System.out.println(String.format("       %s  %s/%s %s",
                        item.getShift().getTitle(), collect.size(), item.getSize(), collect));
            });
        }
        System.out.println();
        Integer sum = allResultItems.stream().map(ResultItem::getSize).reduce(Integer::sum).orElse(0);
        System.out.println(String.format("参与人数：%s，总天数：%s，每日班次数：%s，总班次数：%s，班次人总数：%s",
                users.size(), resultTable.rowKeySet().size(), resultTable.columnKeySet().size(),
                allResultItems.size(), sum));
        System.out.println(String.format("失败单元格数: %s， 获取人数无法使用次数：%s，总获取人员次数：%s，最大完成错误次数：%s",
                cellErrorNumber, handler.getErrorSum(), getPeopleNumber, maxRetryNumber));
        System.out.println();
        System.out.println();
    }

    /**
     * 填充人员信息
     *
     * @param item 需要填充人员的班次对象
     */
    public void fillUser(ResultItem<SHIFT, USER> item) {
        LocalDate rowKey = item.getDay();
        Object colKey = item.getShiftKey();
        int retryNumber = 0;
        while (item.canAdd()) {
            AbstractPlanUser<USER> people = getOneUser(retryNumber);
            // 人员是否可用，判断人员是否可以在该班次上班
            boolean success = handler.isSuccess(rowKey, colKey, people, retryNumber, openLogger);

            if (!success) {
                retryNumber++;
            } else {
                // 判断是否进行过重试操作，如果是，则记录一些数据
                if (retryNumber > 0) {
                    cellErrorNumber++;
                    if (openLogger && isDebug) {
                        logger.debug("第{}个需要重试的单元格，本次成功最大失败次数：{} {} {} 员工当前可用：{} 成功员工：{}", cellErrorNumber,
                                retryNumber, rowKey, colKey, people.canUse(), people);
                    }
                }
                if (retryNumber > maxRetryNumber) {
                    maxRetryNumber = retryNumber;
                }
                // 重置当前错误次数
                retryNumber = 0;
                // 向当前班次添加人员
                item.addUser(people);
            }
        }
    }

    /**
     * 随机获取一个人员信息
     *
     * @param retryNumber 重试次数
     * @return 返回一个用户信息
     */
    public AbstractPlanUser<USER> getOneUser(int retryNumber) {
        // 获得所有可使用的人员列表
        List<AbstractPlanUser<USER>> canUseUsers = users.stream()
                .filter(AbstractPlanUser::canUse).collect(Collectors.toList());
        if (canUseUsers.isEmpty()) {
            throw new RuntimeException("无可用人员，无法继续完成排班");
        }
        int size = canUseUsers.size();
        // 最大重试次数为两倍人员数，在第1个 canUseUsers.size() 内会随机排序获取，在第2个 canUseUsers.size() 内会从头到尾按顺序获取
        if (retryNumber >= size * 2) {
            throw new RuntimeException("当前班次获取人员次数已经达到上限，继续获取也同样无法完成当前任务");
        }
        // 在重试次数为 用户 数量以内时，默认获取第 0 条数据（随机乱序，按使用次数再排序）
        int index = retryNumber / size;
        if (retryNumber >= size) {
            // 当重试次数大于 用户数量 时，尝试把所有用户都获取一次，如果所有用户都无法满足，隐藏只能判断失败抛出异常
            index = retryNumber % size;
        } else {
            // 人员乱序
            Collections.shuffle(canUseUsers);
            // 对使用次数进行排序
            canUseUsers.sort(Comparator.comparingInt(AbstractPlanUser::getUseNumber));
        }
        getPeopleNumber++;
        return canUseUsers.get(index);
    }

    /**
     * 循环获取排班结果
     *
     * @param consumer
     */
    public void resultForEach(Consumer3<LocalDate, AbstractPlanShift<SHIFT>, AbstractPlanUser<USER>> consumer) {
        // 拿到所有的结果
        for (ResultItem<SHIFT, USER> item : allResultItems) {
            AbstractPlanShift<SHIFT> shift = item.getShift();
            List<AbstractPlanUser<USER>> users = item.getUsers();
            for (AbstractPlanUser<USER> user : users) {
                consumer.accept(item.getDay(), shift, user);
            }
        }
    }
}
